All files / src/app/api/sessions/[id]/restore route.ts

100% Statements 17/17
100% Branches 8/8
100% Functions 1/1
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44                                    8x 8x 8x 7x 7x 7x 6x 6x 4x 1x                 3x 3x 2x   1x      
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
import { getDb } from "@/lib/db";
import { requireApiCollectionAccess } from "@/server/dal/collection-access";
import { SessionController } from "@/server/api/sessions";
import { errorResponse } from "@/server/api/http-errors";
 
// Undo "set aside": re-activate a soft-deleted session. The active
// GET/PATCH/DELETE handlers 404 on soft-deleted rows, so restore needs its own
// resolver that reaches set-aside sessions (getSessionIncludingDeletedOrNull).
//
// Authz mirrors authorizeSession's order — existence -> collection gate -> curator
// gate -> action. The curator gate is what closes the IDOR (purses-t9l): a
// DIFFERENT curator who IS a member of the shared collection passes the collection
// gate, so ONLY the curatorId check stops them from restoring someone else's
// set-aside session. A null lookup is 404 (truly absent); a thrown DB error
// propagates (-> 500), never masked as 404.
export async function POST(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const session = await getSession();
  if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  const db = getDb();
  const sortSession = await SessionController.fromDb(db).getSessionIncludingDeletedOrNull(id);
  if (!sortSession) return NextResponse.json({ error: "Not found" }, { status: 404 });
  const access = await requireApiCollectionAccess(db, sortSession.collectionId, session.user.id);
  if (access instanceof NextResponse) return access;
  if (sortSession.curatorId !== session.user.id) {
    return NextResponse.json(
      { error: "You can only restore your own sorting session" },
      { status: 403 },
    );
  }
  // Auth (401/404/collection/curator) is resolved ABOVE, before the try — those
  // are deliberate status responses, not errors to map. Only the restore itself
  // can raise CurrentSessionExistsError (a newer in-progress session now holds
  // the slot), which errorResponse turns into a gentle 409 instead of a raw 500.
  try {
    const restored = await SessionController.fromDb(db).restoreSession(id);
    return NextResponse.json(restored);
  } catch (error) {
    return errorResponse(error); // CurrentSessionExistsError -> 409
  }
}